package com.cx.repository;

import com.cx.config.LettuceRedisAdapter;
import com.cx.entity.RedisEntity;
import com.cx.property.LettuceProperties;
import com.cx.support.AsyncCallback;
import com.cx.utils.BeanHelper;
import com.cx.utils.CacheConsts;
import com.cx.utils.JsonHelper;
import com.cx.utils.SerializeAdapter;
import com.google.common.collect.Lists;
import io.lettuce.core.api.async.*;
import io.lettuce.core.api.reactive.RedisReactiveCommands;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.support.RepositoryProxyPostProcessor;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;

import javax.persistence.NamedNativeQueries;
import javax.persistence.NamedNativeQuery;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

public class SecurecyPostProcessor<T extends RedisEntity<ID>, ID extends Serializable> implements RepositoryProxyPostProcessor {

    private final LettuceRedisAdapter lra;

    public SecurecyPostProcessor(LettuceRedisAdapter lra) {
        this.lra = lra;
    }

    @Override
	public void postProcess(ProxyFactory factory, RepositoryInformation information) {
		factory.addAdvice(new SecurecyAdvice<T, ID>((Class<T>)information.getDomainType(), information, lra));
	}

    private class SecurecyAdvice<T extends RedisEntity<ID>, ID extends Serializable> implements MethodInterceptor {

        private final Class<T> clazz;

        private final String clazzName;

        private final RepositoryInformation information;

        private final LettuceRedisAdapter lettuceRedisAdapter;

        private final LettuceProperties lettuceProperties;

        public SecurecyAdvice(Class<T> domainClass, RepositoryInformation information, LettuceRedisAdapter lra) {
            this.clazz = domainClass;
            this.clazzName = domainClass.getSimpleName().toLowerCase();
            this.information = information;
            this.lettuceRedisAdapter = lra;
            this.lettuceProperties = SerializeAdapter.lettuceProperties();
        }

        @Override
        public Object invoke(final MethodInvocation invocation) throws Throwable {

            final Method method = invocation.getMethod();
            final String methodName = method.getName();
            final int lenth = methodName.length();
            final boolean isQueryAnnotationPresent = method.isAnnotationPresent(Query.class);
            final boolean isNamedQueryAnnotationPresent = method.isAnnotationPresent(NamedQuery.class);
            final boolean isNamedQueriesAnnotationPresent = method.isAnnotationPresent(NamedQueries.class);
            final boolean isNamedNativeQueryAnnotationPresent = method.isAnnotationPresent(NamedNativeQuery.class);
            final boolean isNamedNativeQueriesAnnotationPresent = method.isAnnotationPresent(NamedNativeQueries.class);
            final boolean isAssignableFromBaseJpaRedisRepositor = BaseJpaRedisRepository.class.isAssignableFrom(information.getRepositoryInterface());
            final String findEntity = new StringBuilder("find"+clazz.getSimpleName()).toString();
            if(isAssignableFromBaseJpaRedisRepositor && (methodName.startsWith("findBy") || methodName.startsWith(findEntity)
                    || isQueryAnnotationPresent
                    || isNamedQueryAnnotationPresent
                    || isNamedQueriesAnnotationPresent
                    || isNamedNativeQueryAnnotationPresent
                    || isNamedNativeQueriesAnnotationPresent)) {

                final String idskey = BeanHelper.key(keyspace(), "findBy",
                        new String[]{methodName.startsWith(findEntity)?methodName.replace(findEntity, ""):methodName.substring(6, lenth)}, invocation.getArguments());
                final Type returnType = method.getGenericReturnType();

                if(returnType.getTypeName().startsWith("java.util.List")){
                    final ParameterizedType paramType = (ParameterizedType)returnType;
                    final Type[] types = paramType.getActualTypeArguments();
                    if(ArrayUtils.isNotEmpty(types)){
                        if(types[0].getTypeName().equals(clazz.getTypeName())){
                            final List<T> list = findByToRedis(idskey);
                            if (!CollectionUtils.isEmpty(list)) {
                                return list;
                            }
                        }else{
                            final List<Object> data = lettuceRedisAdapter.lrange(idskey);//(Class<?>) types[0]
                            if (!CollectionUtils.isEmpty(data)) {
                                return data.get(0);
                            }
                        }
                    }else {
                        final List<Object> data = lettuceRedisAdapter.lrange(idskey);
                        if (!CollectionUtils.isEmpty(data)) {
                            return data.get(0);
                        }
                    }
                } else {
                    if(returnType.getTypeName().equals(clazz.getTypeName())){
                        final List<T> list = findByToRedis(idskey);
                        if (!CollectionUtils.isEmpty(list)) {
                            return list.get(0);
                        }
                    }else{
                        final List<Object> data = lettuceRedisAdapter.lrange(idskey);
                        if (!CollectionUtils.isEmpty(data)) {
                            return data.get(0);
                        }
                    }
                }

                final Object obj = invocation.proceed();
                if(ObjectUtils.isEmpty(obj)){
                    return obj;
                }

                final Map<String, String> delingCacheKeys = isQueryAnnotationPresent ?
                        CacheConsts.delingCacheKeys(method.getAnnotation(Query.class).value()) :
                            (isNamedQueryAnnotationPresent ?
                                    CacheConsts.delingCacheKeys(method.getAnnotation(NamedQuery.class).query()) :
                                        (isNamedQueriesAnnotationPresent ?
                                            CacheConsts.batchDelingCacheKeys(method.getAnnotation(NamedQueries.class).value()) :
                                                (isNamedNativeQueryAnnotationPresent ?
                                                        CacheConsts.delingCacheKeys(method.getAnnotation(NamedNativeQuery.class).query()) :
                                                            (isNamedNativeQueriesAnnotationPresent ?
                                                                    CacheConsts.batchDelingCacheKeys(method.getAnnotation(NamedNativeQueries.class).value()) : null))));
                if(returnType.getTypeName().startsWith("java.util.List")){
                    final ParameterizedType paramType = (ParameterizedType)returnType;
                    final Type[] types = paramType.getActualTypeArguments();
                    if(ArrayUtils.isNotEmpty(types)){
                        if(types[0].getTypeName().equals(clazz.getTypeName())){
                            final List<T> list = (List<T>)obj;
                            saveFindByToRedis(list, idskey, delingCacheKeys);
                            return obj;
                        }else{
                            final List<?> list = (List<?>)obj;
                            saveFindByToRedis(delingCacheKeys, list, idskey);
                            return obj;
                        }
                    }else {
                        final List<?> list = (List<?>)obj;
                        saveFindByToRedis(delingCacheKeys, list, idskey);
                        return obj;
                    }
                } else {
                    if(returnType.getTypeName().equals(clazz.getTypeName())){
                        final List<T> list = Lists.newArrayListWithCapacity(1);
                        final T t = (T)obj;
                        if(!ObjectUtils.isEmpty(t.getId())) {//防止空对象
                            list.add(t);
                        }
                        saveFindByToRedis(list, idskey, delingCacheKeys);
                        return obj;
                    }else{
                        saveFindByToRedis(delingCacheKeys, obj, idskey);
                        return obj;
                    }
                }
            }

            return invocation.proceed();
        }

        /**
         *  特殊查询，先缓存key，调用该查询缓存是哪些ID时，先取出对应缓存key的全部ID值，再去缓存找ID KEY类型的所有缓存值
         * @param idskey
         * @return
         */
        public List<T> findByToRedis(final String idskey) {
            try {
                final List<String> entitykeys = entityKeys(idskey);
                if(!CollectionUtils.isEmpty(entitykeys)) {
                    return getHashDataOfPipelined(entitykeys);
                }

                return null;
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        /**
         *  保存返回结果为List<T> findby到redis
         * @return
         */
        public List<T> saveFindByToRedis(final List<T> entities, final String idskey, final Map<String, String> delingCacheKeys) {
            if (StringUtils.isBlank(idskey) || CollectionUtils.isEmpty(entities)) {
                return entities;
            }

            if (null == entities.get(0) || null == entities.get(0).getId()) {
                return entities;
            }

            try {
                execMultiEntities(entities, idskey, delingCacheKeys);

                return (List<T>) entities;
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        public List<?> saveFindByToRedis(final Map<String, String> delingCacheKeys, final List<?> list, final String idskey) {
            if (CollectionUtils.isEmpty(list)) {
                return list;
            }

            try {
                execMultiLists(delingCacheKeys, list, idskey);

                return list;
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        public Object saveFindByToRedis(final Map<String, String> delingCacheKeys, final Object object, final String idskey) {
            if (null == object) {
                return object;
            }

            try {
                execObject(delingCacheKeys, object, idskey);

                return object;
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        private String keyspace(){
            return lettuceProperties.cacheLevel2Pre() + clazzName;
        }

        public String key(final ID id){
            return keyspace() + ":ids:" + id;
        }

        private List<String> entityKeys(final String key){
            final Boolean hasKey = lettuceRedisAdapter.exists(key);
            if(BooleanUtils.isFalse(hasKey)){
                return null;
            }

            return lettuceRedisAdapter.getlist(key, String.class);
        }

        private T getOnlyOne(final String key){
            final Boolean hasKey = lettuceRedisAdapter.exists(key);
            if(BooleanUtils.isFalse(hasKey)){
                return null;
            }

//            final Map<String, Object> map = lettuceRedisAdapter.hgetall(key);
//            return BeanHelper.fromHash(map, clazz);
            final String entity = lettuceRedisAdapter.get(key, String.class);
            return JsonHelper.deserialize(entity, clazz);
        }

        private List<T> getEntities(final List<Map<String, Object>> result){
            final List<T> list = Lists.newArrayListWithCapacity(result.size());
            if(CollectionUtils.isEmpty(result)){
                return list;
            }
            result.iterator().forEachRemaining(map -> {
                final T entity = BeanHelper.fromHash(map, clazz);
                if (Objects.nonNull(entity) && Objects.nonNull(entity.getId())) {
                    list.add(entity);
                }
            });

            return list;
        }

        private List<T> getNewEntities(final List<String> result){
            final List<T> list = Lists.newArrayListWithCapacity(result.size());
            if(CollectionUtils.isEmpty(result)){
                return list;
            }

            result.iterator().forEachRemaining(t -> {
                final T entity = JsonHelper.deserialize(t, clazz);
                if (Objects.nonNull(entity) && Objects.nonNull(entity.getId())) {
                    list.add(entity);
                }
            });

            return list;
        }

        private List<T> getHashDataOfPipelined(List<String> entitykeys){

            entitykeys = entitykeys.stream().map(id -> {
                return keyspace() + ":ids:" + id;
            }).collect(Collectors.toList());
//            return getEntities(lettuceRedisAdapter.batchHgetall(entitykeys));
            return getNewEntities(lettuceRedisAdapter.batchGet(entitykeys, String.class));
        }

        private void execMultiEntities(final List<T> ts, String idskey, final Map<String, String> delingCacheKeys){

            final List<String> ids = Lists.newArrayListWithCapacity(10);
            putAllToCache(ts, ids);

            lettuceRedisAdapter.batchProcessAsync(new AsyncCallback() {
                @Override
                public void asyncInvoke(RedisAsyncCommands<String, Object> commands) {
                    final RedisStringAsyncCommands<String, Object> stringAsyncCommands = (RedisStringAsyncCommands<String, Object>)commands;
                    final RedisSetAsyncCommands<String, Object> setAsyncCommands = (RedisSetAsyncCommands<String, Object>)commands;
                    if(!CollectionUtils.isEmpty(delingCacheKeys)){
                        for (Map.Entry<String, String> table : delingCacheKeys.entrySet()) {
                            final String delingkey = lettuceProperties.cacheLevel2Pre() + "deling:" + table.getKey();
                            setAsyncCommands.sadd(delingkey, delingCacheKeys.values().toArray());
                        };
                    }
                    stringAsyncCommands.setex(idskey, lettuceProperties.getCacheLevel2Ttl(), ids);
                }

                @Override
                public void asyncInvoke(RedisReactiveCommands<String, Object> commands) {
                    final RedisStringAsyncCommands<String, Object> stringAsyncCommands = (RedisStringAsyncCommands<String, Object>)commands;
                    final RedisSetAsyncCommands<String, Object> setAsyncCommands = (RedisSetAsyncCommands<String, Object>)commands;
                    if(!CollectionUtils.isEmpty(delingCacheKeys)){
                        for (Map.Entry<String, String> table : delingCacheKeys.entrySet()) {
                            final String delingkey = lettuceProperties.cacheLevel2Pre() + "deling:" + table.getKey();
                            setAsyncCommands.sadd(delingkey, delingCacheKeys.values().toArray());
                        };
                    }
                    stringAsyncCommands.setex(idskey, lettuceProperties.getCacheLevel2Ttl(), ids);
                }
            });
        }

        private void execMultiLists(final Map<String, String> delingCacheKeys, final List<?> list, final String idskey){

            lettuceRedisAdapter.batchProcessAsync(new AsyncCallback() {
                @Override
                public void asyncInvoke(RedisAsyncCommands<String, Object> commands) {
                    final RedisKeyAsyncCommands<String, Object> keyAsyncCommands = (RedisKeyAsyncCommands<String, Object>)commands;
                    final RedisSetAsyncCommands<String, Object> setAsyncCommands = (RedisSetAsyncCommands<String, Object>)commands;
                    final RedisListAsyncCommands<String, Object> listAsyncCommands = (RedisListAsyncCommands<String, Object>)commands;
                    if(!CollectionUtils.isEmpty(delingCacheKeys)){
                        for (Map.Entry<String, String> table : delingCacheKeys.entrySet()) {
                            final String delingkey = lettuceProperties.cacheLevel2Pre() + "deling:" + table.getKey();
                            setAsyncCommands.sadd(delingkey, delingCacheKeys.values().toArray());
                        };
                    }
                    listAsyncCommands.lpush(idskey, list);
                    keyAsyncCommands.expire(idskey, lettuceProperties.getCacheLevel2Ttl());
                }

                @Override
                public void asyncInvoke(RedisReactiveCommands<String, Object> commands) {
                    final RedisKeyAsyncCommands<String, Object> keyAsyncCommands = (RedisKeyAsyncCommands<String, Object>)commands;
                    final RedisSetAsyncCommands<String, Object> setAsyncCommands = (RedisSetAsyncCommands<String, Object>)commands;
                    final RedisListAsyncCommands<String, Object> listAsyncCommands = (RedisListAsyncCommands<String, Object>)commands;
                    if(!CollectionUtils.isEmpty(delingCacheKeys)){
                        for (Map.Entry<String, String> table : delingCacheKeys.entrySet()) {
                            final String delingkey = lettuceProperties.cacheLevel2Pre() + "deling:" + table.getKey();
                            setAsyncCommands.sadd(delingkey, delingCacheKeys.values().toArray());
                        };
                    }
                    listAsyncCommands.lpush(idskey, list);
                    keyAsyncCommands.expire(idskey, lettuceProperties.getCacheLevel2Ttl());
                }
            });
        }

        private void execObject(final Map<String, String> delingCacheKeys, final Object object, final String idskey){

            lettuceRedisAdapter.batchProcessAsync(new AsyncCallback() {
                @Override
                public void asyncInvoke(RedisAsyncCommands<String, Object> commands) {
                    final RedisKeyAsyncCommands<String, Object> keyAsyncCommands = (RedisKeyAsyncCommands<String, Object>)commands;
                    final RedisSetAsyncCommands<String, Object> setAsyncCommands = (RedisSetAsyncCommands<String, Object>)commands;
                    final RedisListAsyncCommands<String, Object> listAsyncCommands = (RedisListAsyncCommands<String, Object>)commands;
                    if(!CollectionUtils.isEmpty(delingCacheKeys)){
                        for (Map.Entry<String, String> table : delingCacheKeys.entrySet()) {
                            final String delingkey = lettuceProperties.cacheLevel2Pre() + "deling:" + table.getKey();
                            setAsyncCommands.sadd(delingkey, delingCacheKeys.values().toArray());
                        };
                    }
                    listAsyncCommands.lpush(idskey, object);
                    keyAsyncCommands.expire(idskey, lettuceProperties.getCacheLevel2Ttl());
                }

                @Override
                public void asyncInvoke(RedisReactiveCommands<String, Object> commands) {
                    final RedisKeyAsyncCommands<String, Object> keyAsyncCommands = (RedisKeyAsyncCommands<String, Object>)commands;
                    final RedisSetAsyncCommands<String, Object> setAsyncCommands = (RedisSetAsyncCommands<String, Object>)commands;
                    final RedisListAsyncCommands<String, Object> listAsyncCommands = (RedisListAsyncCommands<String, Object>)commands;
                    if(!CollectionUtils.isEmpty(delingCacheKeys)){
                        for (Map.Entry<String, String> table : delingCacheKeys.entrySet()) {
                            final String delingkey = lettuceProperties.cacheLevel2Pre() + "deling:" + table.getKey();
                            setAsyncCommands.sadd(delingkey, delingCacheKeys.values().toArray());
                        };
                    }
                    listAsyncCommands.lpush(idskey, object);
                    keyAsyncCommands.expire(idskey, lettuceProperties.getCacheLevel2Ttl());
                }
            });
        }

        private void putAllToCache(final Iterable<T> entities, final List<String> ids){
//            final Map<String, Map<String, Object>> dataMap = new HashMap(10);
//            entities.iterator().forEachRemaining(entity -> {
//                ids.add(String.valueOf(entity.getId()));
//
//                dataMap.put(key(entity.getId()), BeanHelper.toHash(entity));
//            });
            final Map<String, Object> dataMap = new HashMap(10);
            entities.iterator().forEachRemaining(entity -> {
                ids.add(String.valueOf(entity.getId()));

                dataMap.put(key(entity.getId()), JsonHelper.serialize(entity));
            });

//            lettuceRedisAdapter.batchHmset(dataMap);
            lettuceRedisAdapter.batchSetex(dataMap);
        }
    }
}